package executives;

import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;

import utils.StateCamera;

import components.AbstractNeuron;

import executives.thread_managers.Consumer;
import executives.thread_managers.SpikeProducer;

/**
 * This class runs the simulation forward through time in parallel, using
 * threads from a list of available threads or creating threads as necessary
 * (based on some specified number of threads) to run the simulation. A 
 * specific spike producer must be specified (which contains the majority
 * of the data for the simulation), otherwise consumers are created on the fly.
 * 
 * @author zach
 *
 */
public class RunForwardParallel implements Callable<Boolean>{

	public static final int DEFAULT_NUM_THREADS =
			Runtime.getRuntime().availableProcessors();
	
	
	/** 
	 * The desired number of threads, defaults to number of available
	 * processors.
	 */
	private int numThreads = DEFAULT_NUM_THREADS;
	
	/** The spike producer which manages tasks. */
	private SpikeProducer spkProducer;
	
	/** The task queue being filled by the spike producer. */
	private BlockingQueue<Runnable> queue;
	
	private static LinkedList<Thread> threads = new LinkedList<Thread>();

	
	/**
	 * 
	 * @param spkProducer
	 */
	public RunForwardParallel(SpikeProducer spkProducer, int numThreads) {
		this.spkProducer = spkProducer;
		this.setNumThreads(numThreads);
		queue = spkProducer.getQueue();
	}
	
	/**
	 * 
	 * @param spkProducer
	 * @param availableThreads
	 */
	public RunForwardParallel(SpikeProducer spkProducer,
			List<Thread> availableThreads) {
		this.spkProducer = spkProducer;
		this.setNumThreads(availableThreads.size());
		queue = spkProducer.getQueue();
	}
	
	/**
	 * 
	 * @param spikeTrain
	 * @param timeStep
	 * @param latch
	 * @param stateCamera
	 * @param inputNeurons
	 * @param autonomousNeurons
	 */
	public RunForwardParallel (double timeStep, double endTime,
			CountDownLatch latch, StateCamera stateCamera,
			AbstractNeuron[] neurons) {
		
		queue = new LinkedBlockingQueue<Runnable>();
		spkProducer = new SpikeProducer(queue, timeStep, endTime, latch,
				stateCamera, neurons, numThreads - 1);
		
	}
	
	/**
	 * 
	 * @param timeStep
	 * @param time
	 * @param endTime
	 * @param latch
	 * @param stateCamera
	 * @param neurons
	 */
	public RunForwardParallel (double timeStep, double time,
			double endTime, CountDownLatch latch, StateCamera stateCamera,
			AbstractNeuron[] neurons) {
		
		queue = new LinkedBlockingQueue<Runnable>();
		spkProducer = new SpikeProducer(queue, timeStep, time, endTime, latch,
				stateCamera, neurons, numThreads - 1);
		
	}
	
	/**
	 * 
	 * @param numThreads
	 * @param timeStep
	 * @param time
	 * @param endTime
	 * @param latch
	 * @param stateCamera
	 * @param neurons
	 */
	public RunForwardParallel (int numThreads, double timeStep, double time,
			double endTime, CountDownLatch latch, StateCamera stateCamera,
			AbstractNeuron[] neurons) {
		this.numThreads = numThreads;
		queue = new LinkedBlockingQueue<Runnable>();
		spkProducer = new SpikeProducer(queue, timeStep, time, endTime, latch,
				stateCamera, neurons, numThreads - 1);
		
	}
	
	/**
	 * Utilizes available threads or allocates (initializes) numThreads number
	 * of threads to run the simulation. Checks to make sure that certain 
	 * requirements such as the available thread list being non-null and
	 * contains more than 1 thread and warns the user if they are trying to run
	 * this on a machine with only one available processor core to the JVM.
	 * After performing these checks and assigning them the proper runnable
	 * targets, this method starts all thread workers. 
	 */
	@Override
	public Boolean call() throws Exception {
		

		if (numThreads == 1) {			
			Scanner keyboard = new Scanner(System.in);
			System.err.println("You only have one available processor" +
					" core, why are you running this in parallel mode?" +
					" Are you sure you wish to continue? (Y/N)" );
			String input = keyboard.next();
			if(input == "N") {
				System.exit(0);
			} else {
				numThreads = numThreads + 1;
			}
			keyboard.close();
		}
	
		if(!threads.isEmpty()){
			for(int i = 0, n = threads.size(); i < n; i++) {
				threads.poll().join();
			}
		}
		
		Thread p = new Thread(spkProducer);
		threads.add(p);
		spkProducer.setNumConsumers(numThreads -1);
		for(int i = 0; i < numThreads - 1; i++) {
			threads.add(new Thread(new Consumer(queue, spkProducer)));
		}			
		
		for(Thread t : threads) {
			t.start();
		}
		
		return true;
		
	}
	
	public void shutdown() {
		try{
			if(!threads.isEmpty()){
				for(int i = 0, n = threads.size(); i < n; i++) {
					threads.poll().join();
				}
			}
		} catch (InterruptedException ie) {
			System.err.println("Threads were interrupted. Were multiple" +
					" instances of RunForwardParallel in use?" +
					" Do not do this.");
			ie.printStackTrace();
			System.exit(1);
		}
	}
	
	/*/********************************************************************
	 * 						GETTERS AND SETTERS							  *
	 **********************************************************************/
	
	public int getNumThreads() {
		return numThreads;
	}

	public void setNumThreads(int numThreads) {
		this.numThreads = numThreads;
	}

	public SpikeProducer getSpkProducer() {
		return spkProducer;
	}

	public void setSpkProducer(SpikeProducer spkProducer) {
		this.spkProducer = spkProducer;
	}
	
	
}
